iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
自我挑戰組

韌體工程師的不只 0x10 個問題系列 第 22

Day 22 - 初探多執行緒

  • 分享至 

  • xImage
  •  

Day 17 的文章曾經為了鋪陳 volatile 關鍵字用法,做了些執行緒的超入門介紹,但執行緒主題其實還有好多內容,就從這篇開始細細探討。

Day 17 文章回顧

Day 17 文章中曾提及程式、行程與執行緒三者關係圖如下:

程式(program)
     ↓ 開始運行
行程/程序(program)   
 |
 | ┌─> 執行緒一(thread 1)
 └─|
   └─> 執行緒二(thread 2)

執行緒的操作

創建執行緒

語法為 thread thread_name(callable),可以把 thread 想成物件,而透過此語法建立出來的執行緒 thread_name 就是一個實例。

新創建的執行緒實例可被 join(),表示該實例對於其對應的執行緒有關聯性。

範例用法如下:

#include <iostream>
#include <thread>

void work() {
    std::cout << "Thread starts working\n";
    std::cout << "Thread finishes working\n";
}

int main() {
    std::thread t(work);

    std::cout << "Main thread is waiting...\n";
    t.join();
    std::cout << "Thread has finished, back to main\n";

    return 0;
}

這裡的主執行緒 main() 執行到一半時,新實例 t 被建立、啟動新的執行緒去跑 work()

執行到 t.join() 時,主執行緒會暫停,等 t 綁定的新執行緒做完後才繼續跑,執行結果如下:

Main thread is waiting...
Thread starts working
Thread finishes working
Thread has finished, back to main

.joinable():檢查是否仍與執行緒有關聯性

在實際 join() 前,可以用 .joinable() 方法檢查該實例與新執行緒是否仍有關聯,範例如下:

#include <iostream>
#include <thread>

void work() {
    std::cout << "Thread starts working\n";
    std::cout << "Thread finishes working\n";
}

int main() {
    std::thread t(work);

    std::cout << "Before join, t.joinable() = " << t.joinable() << '\n';

    std::cout << "Main thread is waiting...\n";
    t.join();

    std::cout << "After join, t.joinable() = " << t.joinable() << '\n';
    std::cout << "Thread has finished, back to main\n";

    return 0;
}

結果如下:

Before join, t.joinable() = 1
Main thread is waiting...
Thread starts working
Thread finishes working
After join, t.joinable() = 0
Thread has finished, back to main

在新執行緒實例 t 剛被創建起來時,這個 t 對於新執行緒有關聯,因此 t.joinable() 的結果為 1;但在經過 t.join()t 就不再對該執行緒有關聯,此時 t.joinable() 的結果變為 0。

.get_id():追蹤執行緒 ID

如果想要追蹤執行緒的 ID,可以用 thread_name.get_id();,例如:

#include <iostream>
#include <thread>

void work() {
    std::cout << "Worker thread id: " << std::this_thread::get_id() << '\n';
    std::cout << "Thread starts working\n";
    std::cout << "Thread finishes working\n";
}

int main() {
    std::thread t(work);

    std::cout << "Main thread id:   " << std::this_thread::get_id() << '\n';
    std::cout << "New thread id:    " << t.get_id() << '\n';

    std::cout << "Main thread is waiting...\n";
    t.join();
    std::cout << "Thread has finished, back to main\n";

    return 0;
}

顯示結果為:

Main thread id:   129810305829760
New thread id:    129810305816256
Main thread is waiting...
Worker thread id: 129810305816256
Thread starts working
Thread finishes working
Thread has finished, back to main

由此可知主執行緒 ID 為 129810305829760、新執行緒 ID 為 129810305816256,但這並非固定值,每次執行的 ID 都可能會變,因此使用 .get_id() 能做的是「在當次執行區分不同執行緒」,不能用拿到的 ID 值做任何商業邏輯。

.detach():讓新執行緒在背景跑

剛剛的範例是讓主執行緒等新執行緒跑完才繼續跑,而如果希望讓主執行緒不等新執行緒跑完就繼續執行,可以用 thread_name.detach(); 讓新執行緒在背景跑,範例如下:

#include <iostream>
#include <thread>

void work() {
    std::cout << "Thread starts working\n";
    std::cout << "Thread finishes working\n";
}

int main() {
    std::thread t(work);

    std::cout << "Main thread is not waiting...\n";
    t.detach();     // 主執行緒不等新執行緒跑完就繼續跑
    std::cout << "Main thread continues immediately\n";

    return 0;
}

這裡使用 t.detach(); 表示新執行緒在背景跑,主執行緒不會等它跑完,因此結果如下:

Main thread is not waiting...
Main thread continues immediately
Thread starts working
Thread finishes working

主執行緒的 Main thread continues immediately 先跑完,才跑新執行緒裡的 Thread starts workingThread finishes working

值得留意的是,如果主執行緒很快就結束,那整個程式都會跟著結束,不會等在背景跑的新執行緒跑完。

在經過 t.detach() 後,t 也會喪失對該執行緒的關聯性,因此 t.joinable() 也會變成 0。

傳值與傳參考的比較

接下來兩個範例都會傳參數給執行緒。

首先是傳值:

#include <iostream>
#include <thread>

void printNumber(int x) {
    x += 10;
    std::cout << "Inside thread, x = " << x << '\n';
}

int main() {
    int num = 5;

    std::thread t(printNumber, num);
    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

這裡的新執行緒 tnum 變數的值(也就是 main() 中定義的 5)傳到 printNumber() 中,但因為傳的是值的副本,所以 printNumber() 中的 x += 10 只會改到新執行緒的 x,不會連主執行緒的 num 一起改,因此執行結果為:

Inside thread, x = 15
In main, num = 5

然而,若如果改回傳參考如下:

#include <iostream>
#include <thread>
#include <functional>    // std::ref(num) 定義在 <functional> 函式庫中

void addTen(int& x) {    // 傳入的為位址
    x += 10;             // 會改到主執行緒 main() 中的 num
    std::cout << "Inside thread, x = " << x << '\n';
}

int main() {
    int num = 5;

    std::thread t(addTen, std::ref(num));  // 特別指名要傳的是 num 的參考,而非副本
    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

這裡的新執行緒 t 有指名傳入參數為 num 的位址 ref(num),因此 addTen() 裡面的 x += 10 會改到主執行緒的 num,執行結果如下:

Inside thread, x = 15
In main, num = 15

使用 Lambda 表達式

剛剛的範例都可以用 Day 13 介紹的 Lambda 表達式取代,例如「創建執行緒」的範例可以改成:

#include <iostream>
#include <thread>

int main() {
    std::thread t([]() {       // 改成 Lambda 表達式寫法
        std::cout << "Thread starts working\n";
        std::cout << "Thread finishes working\n";
    });

    std::cout << "Main thread is waiting...\n";
    t.join();
    std::cout << "Thread has finished, back to main\n";

    return 0;
}

此範例沒有任何參數,接著將依序討論用「捕獲」與「參數」傳「值」跟「參考」各自的寫法。

用 Lambda 表達式傳值

以「傳值」傳參數的範例可以改成:

#include <iostream>
#include <thread>

int main() {
    int num = 5;

    std::thread t([](int num) { // 函數改用 Lambda 表達式,以參數傳入 num 的副本
        num += 10;  // 修改 num 副本的值
        std::cout << "Inside thread, num = " << num << '\n';
    }, num);  // num 為要傳進 Lambda 表達式的參數

    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

或者用捕獲的方式可以寫成:

#include <iostream>
#include <thread>

int main() {
    int num = 5;

    std::thread t([num]() mutable { // 傳進的是 num 的副本,要加 mutable 才能改動副本
        num += 10;  // 改變的是副本
        std::cout << "Inside thread, num = " << num << '\n';
    });

    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

以上兩範例的執行結果皆如下:

Inside thread, num = 15
In main, num = 5

用 Lambda 表達式傳參考

以「傳參考」傳參數的範例可以改成:

#include <iostream>
#include <thread>
#include <functional>

int main() {
    int num = 5;

    std::thread t([](int& num) {  // 用參數傳 x 的參考進去
        num += 10; // 直接改動外面的 num = 5
        std::cout << "Inside thread, num = " << num << '\n';
    }, std::ref(num));

    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

或者用捕獲的方式可以寫成:

#include <iostream>
#include <thread>

int main() {
    int num = 5;

    std::thread t([&num]() {  // 用捕獲傳 num 的參考進去
        num += 10;  // 直接改動外面的 num = 5
        std::cout << "Inside thread, num = " << num << '\n';
    });

    t.join();

    std::cout << "In main, num = " << num << '\n';
    return 0;
}

這兩種寫法的執行結果都是:

Inside thread, num = 15
In main, num = 15

觀察:執行緒執行順序

如果有很多新執行緒,如下:

#include <iostream>
#include <thread>

void printMessage(int id) {
    std::cout << "Thread " << id << " is running\n";
}

int main() {
    std::thread t1(printMessage, 1);
    std::thread t2(printMessage, 2);
    std::thread t3(printMessage, 3);

    t3.join();
    t1.join();
    t2.join();

    std::cout << "All threads finished\n";
    return 0;
}

會出現什麼結果呢?

主執行緒的 All threads finished 一定會最後顯示,但三個新執行緒到底是誰先執行,完全看當下作業系統怎麼排,並不是先寫的就先執行,所以顯示結果可能為:

Thread 1 is running
Thread 2 is running
Thread 3 is running
All threads finished

也可能是:

Thread 3 is running
Thread 1 is running
Thread 2 is running
All threads finished

或者:

Thread 2 is running
Thread 3 is running
Thread 1 is running
All threads finished

甚至可能是:

Thread Thread 1 is running
Thread 3 is running
2 is running
All threads finished

看起來很混亂,但這也顯示多個執行緒共享資源時,結果非常不可預期,不只執行順序不固定,連輸出都可能被切開、交錯,因此,後面會再介紹「同步(synchronization)機制來避免這樣的執行方式。

參考資料

  1. Multithreading in C++ (GeeksforGeeks)
  2. 多執行緒 — C++ Thread — C++ Thread(李謦伊 Medium)
  3. Threads in C++ The interleaving pattern (Stack Overflow)

上一篇
Day 21 - 智慧型指標
系列文
韌體工程師的不只 0x10 個問題22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言